# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2000, 2002, 2005 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307	USA

from math import pi, floor, atan2, ceil

from traceback import print_stack

from Sketch.const import SelectSet, SelectAdd, SelectSubtract, SelectDrag, \
     Button1Mask, ConstraintMask,\
     SCRIPT_GET, SCRIPT_OBJECT, SCRIPT_OBJECTLIST, SCRIPT_UNDO
from Sketch.warn import pdebug, warn, INTERNAL
from Sketch import Point, Polar, Rect, EmptyRect, UnionRects, PointsToRect
from Sketch import _, _sketch, CreatePath, config, RegisterCommands, \
     CreateMultiUndo, NullUndo, Undo

from Sketch.UI.command import AddCmd
import Sketch.UI.skpixmaps
pixmaps = Sketch.UI.skpixmaps.PixmapTk

from Sketch._sketch import ContAngle, ContSmooth, ContSymmetrical, \
     SelNone, SelNodes, SelSegmentFirst, SelSegmentLast, Bezier, Line

import handle
from base import Primitive, Creator, Editor
from blend import Blend, BlendPaths, MismatchError
from properties import DefaultGraphicsProperties

#
# PolyBezier
#
# The most important primitive, since it can be used to draw lines,
# curves and arbitrary contours (to some degree; circles for instance
# can only be approximated, but that works quite well).
#

def undo_path(undo, path):
    if undo is not None:
        return apply(getattr(path, undo[0]), undo[1:])
    else:
        return undo

class PolyBezier(Primitive):

    is_Bezier	  = 1
    has_edit_mode = 1
    is_curve	  = 1
    is_clip	  = 1

    script_access = Primitive.script_access.copy()

    def __init__(self, paths = None, properties = None, duplicate = None):
        if duplicate is not None:
            if paths is None:
                paths = []
                for path in duplicate.paths:
                    paths.append(path.Duplicate())
                self.paths = tuple(paths)
            else:
                # This special case uses the properties kwarg now, I
                # hope.
                warn(INTERNAL, 'Bezier object created with paths and duplicte')
                print_stack()
                if type(paths) != type(()):
                    paths = (paths,)
                self.paths = paths
        elif paths is not None:
            if type(paths) != type(()):
                paths = (paths,)
            self.paths = paths
        else:
            self.paths = (CreatePath(),)

        Primitive.__init__(self, properties = properties, duplicate=duplicate)

    def Hit(self, p, rect, device, clip = 0):
        for path in self.paths:
            if path.hit_point(rect):
                return 1
        return device.MultiBezierHit(self.paths, p, self.properties,
                                     clip or self.Filled(),
                                     ignore_outline_mode = clip)

    def do_undo(self, undo_list):
        undo = map(undo_path, undo_list, self.paths)
        self._changed()
        return (self.do_undo, undo)

    def Transform(self, trafo):
        undo = []
        undostyle = NullUndo
        try:
            rect = self.bounding_rect
            for path in self.paths:
                undo.append(path.Transform(trafo))
            self._changed()
            undo = (self.do_undo, undo)
            self.update_rects() # calling update_rects directly is a bit faster
            undostyle = Primitive.Transform(self, trafo,
                                            rects = (rect, self.bounding_rect))
            return CreateMultiUndo(undostyle, undo)
        except:
            Undo(undostyle)
            if type(undo) != type(()):
                undo = (self.do_undo, undo)
            Undo(undo)
            raise

    def Translate(self, offset):
        for path in self.paths:
            path.Translate(offset)
        self._changed()
        return self.Translate, -offset

    def DrawShape(self, device, rect = None, clip = 0):
        Primitive.DrawShape(self, device)
        device.MultiBezier(self.paths, rect, clip)

    def GetObjectHandle(self, multiple):
        if self.paths:
            return self.paths[0].Node(0)
        return Point(0, 0)

    def GetSnapPoints(self):
        points = []
        for path in self.paths:
            points = points + path.NodeList()
        return points

    def Snap(self, p):
        found = [(1e100, p)]
        for path in self.paths:
            t = path.nearest_point(p.x, p.y)
            if t is not None:
                #print p, t
                p2 = path.point_at(t)
                found.append((abs(p - p2), p2))
        return min(found)

    def update_rects(self):
        if not self.paths:
            # this should never happen...
            self.coord_rect = self.bounding_rect = Rect(0, 0, 0, 0)
            return

        rect = self.paths[0].accurate_rect()
        for path in self.paths[1:]:
            rect = UnionRects(rect, path.accurate_rect())
        self.coord_rect = rect
        if self.properties.HasLine():
            rect = self.add_arrow_rects(rect)
            self.bounding_rect = rect.grown(self.properties.GrowAmount())
        else:
            self.bounding_rect = rect

    def add_arrow_rects(self, rect):
        if self.properties.HasLine():
            arrow1 = self.properties.line_arrow1
            arrow2 = self.properties.line_arrow2
            if arrow1 or arrow2:
                width = self.properties.line_width
                for path in self.paths:
                    if not path.closed and path.len > 1:
                        if arrow1 is not None:
                            type, controls, p3, cont = path.Segment(1)
                            p = path.Node(0)
                            if type == Bezier:
                                dir = p - controls[0]
                            else:
                                dir = p - p3
                            rect = UnionRects(rect,
                                              arrow1.BoundingRect(p,dir,width))
                        if arrow2 is not None:
                            type, controls, p, cont = path.Segment(-1)
                            if type == Bezier:
                                dir = p - controls[1]
                            else:
                                dir = p - path.Node(-2)
                            rect = UnionRects(rect,
                                              arrow2.BoundingRect(p,dir,width))
        return rect

    def Info(self):
        nodes = 0
        for path in self.paths:
            nodes = nodes + path.len
            if path.closed:
                nodes = nodes - 1
        return _("PolyBezier (%(nodes)d nodes in %(paths)d paths)") \
               % {'nodes':nodes, 'paths':len(self.paths)}

    def set_paths(self, paths):
        undo = (self.set_paths, self.paths)
        self.paths = tuple(paths)
        self._changed()
        return undo

    SetPaths = set_paths
    def Paths(self):
        return self.paths

    def PathsAsObjects(self):
        result = []
        for path in self.paths:
            object = self.__class__(paths = (path.Duplicate(),),
                                    properties = self.properties.Duplicate())
            result.append(object)
        return result
    script_access['PathsAsObjects'] = SCRIPT_OBJECTLIST

    def AsBezier(self):
        # is `return self' enough ?
        return self.Duplicate()

    #
    #
    #

    def SaveToFile(self, file):
        Primitive.SaveToFile(self, file)
        file.PolyBezier(self.paths)

    def load_straight(self, *args):
        apply(self.paths[-1].AppendLine, args)

    def load_curve(self, *args):
        apply(self.paths[-1].AppendBezier, args)

    def load_close(self, copy_cont_from_last = 0):
        self.paths[-1].load_close(copy_cont_from_last)

    def guess_continuity(self):
        for path in self.paths:
            path.guess_continuity()

    def load_IsComplete(self):
        if self.paths[0].len < 2:
            # we need at least two nodes
            return 0
        return 1

    def Blend(self, other, frac1, frac2):
        if self.__class__ != other.__class__:
            try:
                other = other.AsBezier()
                if not other:
                    raise MismatchError
            except AttributeError, value:
                if value == 'AsBezier':
                    raise MismatchError
                else:
                    raise

        paths = BlendPaths(self.paths, other.paths, frac1, frac2)
        blended = PolyBezier(paths = paths)
        self.set_blended_properties(blended, other, frac1, frac2)
        return blended

    def Editor(self):
        return PolyBezierEditor(self)



class PolyBezierCreator(Creator):

    creation_text = _("Create Curve")

    def __init__(self, start):
        self.path = CreatePath()
        Creator.__init__(self, start)

    def apply_constraints(self, p, state):
        if self.path.len > 0:
            node = self.path.Node(-1)
        elif self.dragging:
            node = self.drag_start
        else:
            return p

        if state & ConstraintMask:
            radius, angle = (p - node).polar()
            pi12 = pi / 12
            angle = pi12 * floor(angle / pi12 + 0.5)
            p = node + Polar(radius, angle)
        return p

    def ButtonDown(self, p, button, state):
        p = self.apply_constraints(p, state)
        if self.path.len == 0:
            self.path.AppendLine(p)
        else:
            self.path.AppendBezier(self.drag_cur, p, p)
        return self.DragStart(p)

    def MouseMove(self, p, state):
        if not (state & Button1Mask):
            return
        self.DragMove(self.apply_constraints(p, state))

    def ButtonUp(self, p, button, state):
        if not (state & Button1Mask):
            return
        p = self.apply_constraints(p, state)
        self.DragStop(p)
        if self.path.len > 1:
            type, (p1, p2), p, cont = self.path.Segment(-1)
            p2 = adjust_control_point(p2, p, self.drag_cur, ContSymmetrical)
            self.path.SetBezier(-1, p1, p2, p, ContSymmetrical)

    def EndCreation(self):
        return self.path.len > 1

    def AppendInteractive(self, p):
        return self

    def ContinueCreation(self):
        return self.AppendInteractive

    def DrawDragged(self, device, partially):
        if not partially:
            self.path.draw_not_last(device.Bezier, device.Line)
        device.DrawHandleLine(self.path.Node(-1), self.drag_cur)
        device.DrawSmallRectHandle(self.drag_cur)
        if self.path.len > 1:
            type, (p1, p2), p, cont = self.path.Segment(-1)
            p2 = adjust_control_point(p2, p, self.drag_cur, ContSymmetrical)
            device.Bezier(self.path.Node(-2), p1, p2, p)
            device.DrawHandleLine(p, p2)
            device.DrawSmallRectHandle(p2)

    def CreatedObject(self):
        return PolyBezier(paths = (self.path,),
                          properties = DefaultGraphicsProperties())



class PolyLineCreator(Creator):

    creation_text = _("Create Poly-Line")

    def __init__(self, start):
        self.path = CreatePath()
        self.was_dragged = 0
        Creator.__init__(self, start)

    def apply_constraints(self, p, state):
        if self.path.len > 0:
            node = self.path.Node(-1)
        elif self.dragging:
            node = self.drag_start
        else:
            return p

        if state & ConstraintMask:
            radius, angle = (p - node).polar()
            pi12 = pi / 12
            angle = pi12 * floor(angle / pi12 + 0.5)
            p = node + Polar(radius, angle)
        return p

    def ButtonDown(self, p, button, state):
        return self.DragStart(self.apply_constraints(p, state))

    def MouseMove(self, p, state):
        if not (state & Button1Mask):
            return
        self.was_dragged = 1
        self.DragMove(self.apply_constraints(p, state))

    def ButtonUp(self, p, button, state):
        if not (state & Button1Mask):
            return
        self.DragStop(self.apply_constraints(p, state))
        if self.was_dragged and self.path.len == 0:
            self.path.AppendLine(self.drag_start)
        self.path.AppendLine(self.drag_cur)

    def EndCreation(self):
        return self.path.len > 1

    def AppendInteractive(self, p):
        return self

    def ContinueCreation(self):
        return self.AppendInteractive

    def DrawDragged(self, device, partially):
        if self.path.len > 1:
            if not partially:
                device.DrawBezierPath(self.path)
        if self.path.len >= 1:
            device.Line(self.path.Node(-1), self.drag_cur)
        else:
            if self.was_dragged:
                device.Line(self.drag_start, self.drag_cur)

    def CreatedObject(self):
        return PolyBezier(paths = (self.path,),
                          properties = DefaultGraphicsProperties())

SelCurvePoint = -1

class PolyBezierEditor(Editor):

    EditedClass = PolyBezier
    commands = []

    def __init__(self, object):
        self.selected_path = -1
        self.selected_idx = -1
        self.selection_type = SelNone
        self.other_segment = -1
        Editor.__init__(self, object)
        self.Deselect()

    def SelectPoint(self, p, rect, device, mode):
        self.deselect()
        found = []
        for i in range(len(self.paths)):
            path = self.paths[i]
            t = path.nearest_point(p.x, p.y)
            if t is not None:
                p2 = path.point_at(t)
                found.append((abs(p - p2), i, t, p2))
        dist, i, t, p2 = min(found)
        self.selected_path = i
        self.selected_idx = t
        self.selection_type = SelCurvePoint
        return 1

    def SelectHandle(self, handle, mode = SelectSet):
        path_idx, segment = handle.code
        if segment < 0:
            segment = -segment
            if segment % 2:
                self.selection_type = SelSegmentFirst
            else:
                self.selection_type = SelSegmentLast
            self.selected_idx = (segment + 1) / 2
            segment = segment / 2
        else:
            self.selection_type = SelNodes
            self.selected_idx = segment
        self.selected_path = path_idx
        path = self.paths[path_idx]
        if mode == SelectSet or mode == SelectDrag:
            if not path.SegmentSelected(segment):
                self.deselect()
            path.SelectSegment(segment)
        elif mode == SelectAdd:
            path.SelectSegment(segment)
        elif mode == SelectSubtract:
            path.SelectSegment(segment, 0)

    def SelectRect(self, rect, mode = SelectSet):
        selected = 0
        for path in self.paths:
            selected = path.select_rect(rect, mode) or selected
        self.selection_type = SelNodes
        return selected

    def SelectAllNodes(self):
        for path in self.paths:
            for i in range(path.len):
                path.SelectSegment(i, 1)
    AddCmd(commands, SelectAllNodes, _("Select All Nodes"))

    def deselect(self):
        for path in self.paths:
            path.deselect()

    def Deselect(self):
        self.deselect()
        self.selected_path = -1
        self.selected_idx = -1
        self.selection_type = SelNone

    def ButtonDown(self, p, button, state):
        if self.selected_path >= 0:
            path = self.paths[self.selected_path]
            if self.selection_type == SelNodes:
                start = path.Node(self.selected_idx)
                self.DragStart(start)
                return p - start
            elif self.selection_type == SelSegmentFirst:
                segment = self.selected_idx
                if segment > 1 \
                   and path.SegmentType(segment - 1) == Bezier\
                   and path.Continuity(segment - 1):
                    self.other_segment = segment - 1
                elif path.closed and segment == 1 \
                     and path.SegmentType(-1) == Bezier\
                     and path.Continuity(-1):
                    self.other_segment = path.len - 1
                else:
                    self.other_segment = -1
                p1 = path.Segment(segment)[1][0]
                self.DragStart(p1)
                return p - p1
            elif self.selection_type == SelSegmentLast:
                segment = self.selected_idx
                self.other_segment = -1
                if path.Continuity(segment):
                    if segment < path.len - 1 \
                       and path.SegmentType(segment + 1) == Bezier:
                        self.other_segment = segment + 1
                    elif path.closed and segment == path.len - 1 \
                         and path.SegmentType(1) == Bezier:
                        self.other_segment = 1
                p2 = path.Segment(segment)[1][1]
                self.DragStart(p2)
                return p - p2
            elif self.selection_type == SelCurvePoint:
                start = path.point_at(self.selected_idx)
                segment = int(ceil(self.selected_idx))
                prev = next = -1
                type = path.SegmentType(segment)
                if type == Bezier:
                    if path.Continuity(segment):
                        if segment < path.len - 1 \
                           and path.SegmentType(segment + 1) == Bezier:
                            next = segment + 1
                        elif path.closed and segment == path.len - 1 \
                             and path.SegmentType(1) == Bezier:
                            next = 1
                    if segment > 1 \
                       and path.SegmentType(segment - 1) == Bezier\
                       and path.Continuity(segment - 1):
                        prev = segment - 1
                    elif path.closed and segment == 1 \
                         and path.SegmentType(-1) == Bezier\
                         and path.Continuity(-1):
                        prev = path.len - 1
                else:
                    if segment < path.len - 1:
                        next = segment + 1
                    elif path.closed and segment == path.len - 1:
                        next = 1
                    if segment >= 1:
                        prev = segment - 1
                    elif path.closed and segment == 1:
                        prev = path.len - 1
                self.other_segment = (prev, next)
                self.DragStart(start)
                return p - start

    def apply_constraints(self, p, state):
        if state & ConstraintMask:
            if self.selection_type == SelNodes:
                pi4 = pi / 4
                off = p - self.drag_start
                d = Polar(pi4 * round(atan2(off.y, off.x) / pi4))
                p = self.drag_start + (off * d) * d
            elif self.selection_type in (SelSegmentFirst, SelSegmentLast):
                path = self.paths[self.selected_path]
                if self.selection_type == SelSegmentFirst:
                    node = path.Node(self.selected_idx - 1)
                else:
                    node = path.Node(self.selected_idx)
                radius, angle = (p - node).polar()
                pi12 = pi / 12
                angle = pi12 * floor(angle / pi12 + 0.5)
                p = node + Polar(radius, angle)
        return p

    def MouseMove(self, p, state):
        self.DragMove(self.apply_constraints(p, state))

    def ButtonUp(self, p, button, state):
        p = self.apply_constraints(p, state)
        self.DragStop(p)
        type = self.selection_type
        if type == SelNodes:
            undo = []
            for path in self.paths:
                if path.selection_count() > 0:
                    undo.append(path.move_selected_nodes(self.off))
                else:
                    undo.append(None)
            if undo:
                self._changed()
                return (self.do_undo, undo)
        elif type in (SelSegmentFirst, SelSegmentLast):
            idx = self.selected_path
            segment = self.selected_idx
            path = self.paths[idx].Duplicate()
            paths = self.paths[:idx] + (path,) + self.paths[idx + 1:]
            if type == SelSegmentFirst:
                type, (p1, p2), node, cont = path.Segment(segment)
                path.SetBezier(segment, self.drag_cur, p2, node, cont)
                if self.other_segment >= 0:
                    other = self.other_segment
                    type, (p1, p2), node, cont = path.Segment(other)
                    p2 = adjust_control_point(p2, node, self.drag_cur, cont)
                    path.SetBezier(other, p1, p2, node, cont)
                path.SelectSegment(segment - 1)
            elif type == SelSegmentLast:
                type, (p1, p2), node, cont = path.Segment(segment)
                path.SetBezier(segment, p1, self.drag_cur, node, cont)
                if self.other_segment >= 0:
                    other = self.other_segment
                    type, (p1, p2), node2, cont2 = path.Segment(other)
                    p1 = adjust_control_point(p1, node, self.drag_cur, cont)
                    path.SetBezier(other, p1, p2, node2, cont2)
                path.SelectSegment(segment)
            return self.set_paths(paths) # set_paths calls _changed()
        elif self.selection_type == SelCurvePoint:
            idx = self.selected_path
            path = self.paths[idx].Duplicate()
            paths = self.paths[:idx] + (path,) + self.paths[idx + 1:]

            segment = int(self.selected_idx)
            t = self.selected_idx - segment
            type, control, node, cont = path.Segment(segment + 1)
            if type == Bezier:
                p1, p2 = control
                if t <= 0.5:
                    alpha = ((t * 2) ** 3) / 2
                else:
                    alpha = 1 - (((1 - t) * 2) ** 3) / 2

                p1 = p1 + (self.off / (3 * t * (1 - t)**2)) * (1 - alpha)
                p2 = p2 + (self.off / (3 * t**2 * (1 - t))) * alpha

                path.SetBezier(segment + 1, p1, p2, node, cont)
            else:
                path.SetLine(segment + 1, node + self.off, cont)
            prev, next = self.other_segment
            if prev >= 0:
                _type, _control, _node, _cont = path.Segment(prev)
                if _type == Bezier:
                    _p1, _p2 = _control
                    if type == Bezier:
                        _p2 = adjust_control_point(_p2, _node, p1, _cont)
                    else:
                        _p2 = _p2 + self.off
                        _node = _node + self.off
                    path.SetBezier(prev, _p1, _p2, _node, _cont)
                else:
                    path.SetLine(prev, _node + self.off, _cont)
            if next >= 0:
                _type, _control, _node, _cont = path.Segment(next)
                if _type == Bezier:
                    _p1, _p2 = _control
                    if type == Bezier:
                        _p1 = adjust_control_point(_p1, node, p2, cont)
                    else:
                        _p1 = _p1 + self.off
                    path.SetBezier(next, _p1, _p2, _node, _cont)
            return self.set_paths(paths) # set_paths calls _changed()

    def DrawDragged(self, device, partially):
        if self.selection_type == SelNodes:
            for path in self.paths:
                path.draw_dragged_nodes(self.off, partially,
                                        device.Bezier, device.Line)
        elif self.selection_type == SelSegmentFirst:
            if not partially:
                for path in self.paths:
                    path.draw_unselected(device.Bezier, device.Line)
            path = self.paths[self.selected_path]
            segment = self.selected_idx
            node = path.Node(segment - 1)
            type, (p1, p2), node2, cont = path.Segment(segment)
            device.Bezier(node, self.drag_cur, p2, node2)
            device.DrawSmallRectHandle(self.drag_cur)
            device.DrawHandleLine(node, self.drag_cur)
            if self.other_segment >= 0:
                other = self.other_segment
                type, (p1, p2), node, cont = path.Segment(other)
                p2 = adjust_control_point(p2, node, self.drag_cur, cont)
                device.Bezier(path.Node(other - 1), p1, p2, node)
                device.DrawSmallRectHandle(p2)
                device.DrawHandleLine(node, p2)
        elif self.selection_type == SelSegmentLast:
            if not partially:
                for path in self.paths:
                    path.draw_unselected(device.Bezier, device.Line)
            path = self.paths[self.selected_path]
            segment = self.selected_idx
            type, (p1, p2), node, cont = path.Segment(segment)
            device.Bezier(path.Node(segment - 1), p1, self.drag_cur, node)
            device.DrawSmallRectHandle(self.drag_cur)
            device.DrawHandleLine(node, self.drag_cur)
            if self.other_segment >= 0:
                other = self.other_segment
                type, (p1, p2), node2, cont2 = path.Segment(other)
                p1 = adjust_control_point(p1, node, self.drag_cur, cont)
                device.Bezier(node, p1, p2, node2)
                device.DrawSmallRectHandle(p1)
                device.DrawHandleLine(node, p1)
        elif self.selection_type == SelCurvePoint:
            path = self.paths[self.selected_path]
            segment = int(self.selected_idx)
            t = self.selected_idx - segment
            prevnode = path.Node(segment)

            type, control, node, cont = path.Segment(segment + 1)
            if type == Bezier:
                p1, p2 = control
                if t <= 0.5:
                    alpha = ((t * 2) ** 3) / 2
                else:
                    alpha = 1 - (((1 - t) * 2) ** 3) / 2

                p1 = p1 + (self.off / (3 * t * (1 - t)**2)) * (1 - alpha)
                p2 = p2 + (self.off / (3 * t**2 * (1 - t))) * alpha

                device.Bezier(prevnode, p1, p2, node)
                device.DrawSmallRectHandle(p1)
                device.DrawHandleLine(prevnode, p1)
                device.DrawSmallRectHandle(p2)
                device.DrawHandleLine(node, p2)
            else:
                device.DrawLine(prevnode + self.off, node + self.off)
            prev, next = self.other_segment
            if prev > 0:
                _type, _control, _node, _cont = path.Segment(prev)
                if _type == Bezier:
                    _p1, _p2 = _control
                    if type == Bezier:
                        _p2 = adjust_control_point(_p2, _node, p1, _cont)
                        device.Bezier(path.Node(prev - 1), _p1, _p2, _node)
                        device.DrawSmallRectHandle(_p2)
                        device.DrawHandleLine(_node, _p2)
                    else:
                        device.Bezier(path.Node(prev - 1), _p1, _p2 + self.off,
                                      _node + self.off)
                else:
                    device.DrawLine(path.Node(prev - 1), _node + self.off)
            if next >= 0:
                _type, _control, _node, _cont = path.Segment(next)
                if _type == Bezier:
                    _p1, _p2 = _control
                    if type == Bezier:
                        _p1 = adjust_control_point(_p1, node, p2, cont)
                        device.Bezier(node, _p1, _p2, _node)
                        device.DrawSmallRectHandle(_p1)
                        device.DrawHandleLine(node, _p1)
                    else:
                        device.Bezier(node + self.off, _p1 + self.off, _p2,
                                      _node)
                else:
                    device.DrawLine(node + self.off, _node)

    def GetHandles(self):
        NodeHandle = handle.MakeNodeHandle
        ControlHandle = handle.MakeControlHandle
        LineHandle = handle.MakeLineHandle
        handles = []
        node_handles = []
        append = handles.append
        for path_idx in range(len(self.paths)):
            path = self.paths[path_idx]
            if path.len > 0:
                if not path.closed:
                    node_handles.append(NodeHandle(path.Node(0),
                                                   path.SegmentSelected(0),
                                                   (path_idx, 0)))
                for i in range(1, path.len):
                    selected = path.SegmentSelected(i)
                    node_handles.append(NodeHandle(path.Node(i), selected,
                                                   (path_idx, i)))
                    if (path.SegmentType(i) == Bezier
                        and (selected or path.SegmentSelected(i - 1))):
                        type, (p1, p2), node, cont = path.Segment(i)
                        append(ControlHandle(p1, (path_idx, -(2 * i - 1))))
                        append(ControlHandle(p2, (path_idx, -(2 * i))))
                        append(LineHandle(path.Node(i - 1), p1))
                        append(LineHandle(p2, node))
        if self.selection_type == SelCurvePoint:
            p = self.paths[self.selected_path].point_at(self.selected_idx)
            handles.append(handle.MakeCurveHandle(p))
        return handles + node_handles

    def Info(self):
        selected = 0
        idx = None
        paths = self.paths
        for i in range(len(paths)):
            path = paths[i]
            count = path.selection_count()
            if count > 0:
                selected = selected + count
                idx = i
        if selected > 1:
            return _("%d nodes in PolyBezier") % selected
        else:
            if idx is not None:
                path = paths[idx]
                for i in range(path.len):
                    if path.SegmentSelected(i):
                        break
                else:
                    warn(INTERNAL, 'Strange selection count')
                    return _("PolyBezier")
                if i == 0:
                    return _("First node of PolyBezier")
                elif i == path.len - 1:
                    return _("Last node of PolyBezier")
                else:
                    return _("1 node of PolyBezier")
            else:
                if self.selection_type == SelCurvePoint:
                    return _("Point on curve at position %.2f") \
                           % self.selected_idx
                else:
                    return _("No Node of PolyBezier")

    #
    #	Special poly bezier protocol: closing, continuity
    #

    def OpenNodes(self):
        if self.selection_type == SelCurvePoint:
            index = self.selected_path
            paths = list(self.paths)
            path = paths[index]
            paths[index:index + 1] = split_path_at(path, self.selected_idx)
            self.selected_idx = int(self.selected_idx) + 1
            self.selection_type = SelNodes
        else:
            paths = []
            for path in self.paths:
                if path.selection_count() >= 1:
                    if path.closed:
                        for i in range(path.len - 1):
                            if path.SegmentSelected(i):
                                start_idx = i
                                break
                    else:
                        start_idx = 0

                    newpath = CreatePath()
                    paths.append(newpath)
                    p = path.Node(start_idx)
                    newpath.AppendLine(p, ContAngle)

                    for i in range(start_idx + 1, path.len):
                        type, control, p, cont = path.Segment(i)
                        if path.SegmentSelected(i):
                            # XXX remove this ?
                            cont = ContAngle
                        newpath.AppendSegment(type, control, p, cont)
                        if path.SegmentSelected(i) and i < path.len - 1:
                            newpath = CreatePath()
                            newpath.AppendLine(p, ContAngle)
                            paths.append(newpath)

                    if start_idx != 0:
                        # the path was closed and the first node was not
                        # selected
                        for i in range(1, start_idx + 1):
                            type, control, p, cont = path.Segment(i)
                            newpath.AppendSegment(type, control, p, cont)
                else:
                    paths.append(path)
        return self.set_paths(paths)
    AddCmd(commands, OpenNodes, _("Cut Curve"), key_stroke = 'c',
           bitmap = pixmaps.BezierOpenNodes)

    def CloseNodes(self):
        # find out if close is possible
        two = 0
        one = 0
        for i in range(len(self.paths)):
            path = self.paths[i]
            selected = path.selection_count()
            if not selected:
                continue
            if (path.closed and selected) or selected not in (1, 2):
                return
            if selected == 1:
                if path.SegmentSelected(0) or path.SegmentSelected(-1):
                    one = one + 1
                    continue
                return
            else:
                if path.SegmentSelected(0) and path.SegmentSelected(-1):
                    two = two + 1
                    continue
                return
        # now, close the nodes
        if one == 2 and two == 0:
            paths = []
            append_to = None
            for path in self.paths:
                if path.selection_count():
                    if append_to:
                        # path is the second of the paths involved
                        end_node = append_to.Node(-1)
                        if path.SegmentSelected(0):
                            for i in range(1, path.len):
                                type, p12, p, cont = path.Segment(i)
                                if end_node is not None and type == Bezier:
                                    p12 = (p12[0] + end_node - path.Node(0),
                                           p12[1])
                                    end_node = None
                                append_to.AppendSegment(type, p12, p, cont)
                        else:
                            for i in range(path.len - 1, 0, -1):
                                type, p12, p3, cont = path.Segment(i)
                                if end_node is not None and type == Bezier:
                                    p12 = (p12[0],
                                           p12[1] + end_node - path.Node(-1))
                                    end_node = None
                                p = path.Node(i - 1)
                                if type == Bezier:
                                    p12 = (p12[1], p12[0])
                                append_to.AppendSegment(type, p12, p,
                                                        path.Continuity(i - 1))
                        continue
                    else:
                        # path is the first of the paths involved
                        if path.SegmentSelected(0):
                            # reverse the path
                            append_to = CreatePath()
                            p = path.Node(-1)
                            append_to.AppendLine(p, ContAngle)
                            for i in range(path.len - 1, 0, -1):
                                type, p12, p3, cont = path.Segment(i)
                                p = path.Node(i - 1)
                                if type == Bezier:
                                    p12 = (p12[1], p12[0])
                                append_to.AppendSegment(type, p12, p,
                                                        path.Continuity(i - 1))
                            path = append_to
                        else:
                            path = append_to = path.Duplicate()
                        append_to.SetContinuity(-1, ContAngle)
                paths.append(path)
            undo = self.set_paths(paths)
        elif one == 0 and two == 1:
            undo_list = []
            for path in self.paths:
                if path.selection_count():
                    undo_list.append(path.ClosePath())
                else:
                    undo_list.append(None)
            undo = (self.object.do_undo, undo_list)
            self._changed()
        else:
            return
        return undo
    AddCmd(commands, CloseNodes, _("Close Nodes"),
           bitmap = pixmaps.BezierCloseNodes)

    def SetContinuity(self, cont):
        new_paths = []
        for path in self.paths:
            if path.selection_count():
                new_paths.append(set_continuity(path, cont))
            else:
                new_paths.append(path)
        return self.set_paths(new_paths)
    AddCmd(commands, 'ContAngle', _("Angle"), SetContinuity,
           args = ContAngle, bitmap = pixmaps.BezierAngle, key_stroke = 'a')
    AddCmd(commands, 'ContSmooth', _("Smooth"), SetContinuity,
           args = ContSmooth, bitmap = pixmaps.BezierSmooth, key_stroke = 's')
    AddCmd(commands, 'ContSymmetrical', _("Symmetrical"), SetContinuity,
           args = ContSymmetrical, bitmap = pixmaps.BezierSymm, key_stroke='y')

    def SegmentsToLines(self):
        if self.selection_type == SelCurvePoint:
            new_paths = list(self.paths)
            path = new_paths[self.selected_path]
            new_paths[self.selected_path] = segment_to_line(path,
                                                            self.selected_idx)
        else:
            new_paths = []
            for path in self.paths:
                if path.selection_count() > 1:
                    new_paths.append(segments_to_lines(path))
                else:
                    new_paths.append(path)
        return self.set_paths(new_paths)
    AddCmd(commands, SegmentsToLines, _("Curve->Line"), key_stroke = 'l',
           bitmap = pixmaps.BezierCurveLine)

    def SegmentsToCurve(self):
        if self.selection_type == SelCurvePoint:
            new_paths = list(self.paths)
            path = new_paths[self.selected_path]
            new_paths[self.selected_path] = segment_to_curve(path,
                                                             self.selected_idx)
        else:
            new_paths = []
            for path in self.paths:
                if path.selection_count() > 1:
                    new_paths.append(segments_to_beziers(path))
                else:
                    new_paths.append(path)
        return self.set_paths(new_paths)
    AddCmd(commands, SegmentsToCurve, _("Line->Curve"), key_stroke = 'b',
           bitmap = pixmaps.BezierLineCurve)

    def DeleteNodes(self):
        new_paths = []
        for path in self.paths:
            if path.selection_count() > 0:
                newpath = delete_segments(path)
            else:
                newpath = path
            if newpath.len > 1:
                new_paths.append(newpath)
            else:
                # all nodes of path have been deleted
                if __debug__:
                    pdebug('bezier', 'path removed')
        if new_paths:
            return self.set_paths(new_paths)
        else:
            if __debug__:
                pdebug('bezier', 'PolyBezier removed')
            self.document.DeselectObject(self.object)
            return self.parent.Remove(self.object)
    AddCmd(commands, DeleteNodes, _("Delete Nodes"),
           bitmap = pixmaps.BezierDeleteNode, key_stroke = ('-', 'Delete'))

    def InsertNodes(self):
        if self.selection_type == SelCurvePoint:
            new_paths = list(self.paths)
            path = new_paths[self.selected_path]
            new_paths[self.selected_path] = insert_node_at(path,
                                                           self.selected_idx)
            self.selected_idx = int(self.selected_idx) + 1
            self.selection_type = SelNodes
        else:
            new_paths = []
            for path in self.paths:
                if path.selection_count() > 1:
                    new_paths.append(insert_segments(path))
                else:
                    new_paths.append(path)
        return self.set_paths(new_paths)
    AddCmd(commands, InsertNodes, _("Insert Nodes"), key_stroke = '+',
           bitmap = pixmaps.BezierInsertNode)

    def ChangeRect(self):
        prop = self.properties
        if prop.IsAlgorithmicFill() or prop.IsAlgorithmicLine() \
           or prop.line_arrow1 is not None or prop.line_arrow2 is not None \
           or self.selection_type == SelCurvePoint:
            return self.bounding_rect

        filled = self.Filled()
        pts = []
        for path in self.paths:
            if path.selection_count():
                for i in range(1, path.len):
                    if path.SegmentSelected(i - 1) or path.SegmentSelected(i):
                        pts.append(path.Node(i - 1))
                        type, p12, p, cont = path.Segment(i)
                        if type == Bezier:
                            p1, p2 = p12
                            pts.append(p1)
                            pts.append(p2)
                        pts.append(p)
                if filled and not path.closed:
                    if path.SegmentSelected(-1):
                        pts.append(path.Node(0))
                    if path.SegmentSelected(0):
                        pts.append(path.Node(-1))
        if pts:
            return PointsToRect(pts).grown(prop.GrowAmount())
        else:
            return EmptyRect

    context_commands = ('SelectAllNodes',)

RegisterCommands(PolyBezierEditor)

def adjust_control_point(p, node, control, continuity):
    if continuity == ContSymmetrical:
        return 2 * node - control
    elif continuity == ContSmooth:
        try:
            d = (control - node).normalized()
            length = abs(p - node)
            return node - length * d
        except ZeroDivisionError:
            # control == node
            return p
    else:
        return p

def subdivide(p0, p1, p2, p3, t = 0.5):
    t2 = 1 - t
    r = t2 * p1 + t * p2
    q1 = t2 * p0 + t * p1
    q2 = t2 * q1 + t * r
    q5 = t2 * p2 + t * p3
    q4 = t2 * r + t * q5
    q3 = t2 * q2 + t * q4
    return q1, q2, q3, q4, q5


def delete_segments(path):
    newpath = CreatePath()
    selected = path.selection_count()
    if (path.closed and selected == path.len - 1) or selected == path.len:
        return newpath
    f13 = 1.0 / 3.0;	f23 = 2.0 / 3.0
    i = 0
    while path.SegmentSelected(i):
        i = i + 1
    if path.closed and i > 0:
        if path.SegmentType(i) == Bezier:
            last_p2 = path.Segment(i)[1][1]
            last_type = Bezier
        else:
            last_p2 = f23 * path.Node(i - 1) + f13 * path.Node(i)
            last_type = Line
    else:
        last_p2 = None
    newpath.AppendLine(path.Node(i), path.Continuity(i))

    seg_p1 = None; seg_type = None
    for i in range(i + 1, path.len):
        type, p12, p, cont = path.Segment(i)
        if type == Bezier:
            p1, p2 = p12
        if path.SegmentSelected(i):
            if seg_type is None:
                seg_type = type
                if type == Bezier:
                    seg_p1 = p1
                else:
                    seg_p1 = f23 * path.Node(i - 1) + f13 * p
        else:
            if seg_type is not None:
                if type == Bezier or seg_type == Bezier:
                    if type == Line:
                        p2 = f13 * path.Node(i - 1) + f23 * p
                    newpath.AppendBezier(seg_p1, p2, p, cont)
                else:
                    newpath.AppendLine(p, cont)
                seg_type = None
            else:
                newpath.AppendSegment(type, p12, p, cont)
    if path.closed:
        if last_p2 is not None:
            if last_type == Bezier or seg_type == Bezier:
                newpath.AppendBezier(seg_p1, last_p2, newpath.Node(0),
                                     newpath.Continuity(0))
            else:
                newpath.AppendLine(newpath.Node(0), newpath.Continuity(0))
        newpath.ClosePath()
    return newpath

def insert_segments(path):
    newpath = CreatePath()
    newpath.AppendLine(path.Node(0), path.Continuity(0))
    newpath.select_segment(0, path.SegmentSelected(0))

    for i in range(1, path.len):
        type, p12, p, cont = path.Segment(i)
        if path.SegmentSelected(i) and path.SegmentSelected(i - 1):
            if type == Line:
                node = 0.5 * path.Node(i - 1) + 0.5 * path.Node(i)
                newpath.AppendLine(node)
                newpath.select_segment(-1)
                newpath.AppendLine(path.Node(i))
                newpath.select_segment(-1)
            else:
                if newpath.Continuity(-1) == ContSymmetrical:
                    newpath.SetContinuity(-1, ContSmooth)
                p1, p2 = p12
                p1, p2, node, p3, p4 = subdivide(path.Node(i - 1), p1, p2, p)
                newpath.AppendBezier(p1, p2, node, ContSymmetrical)
                newpath.select_segment(-1)
                if cont == ContSymmetrical:
                    cont = ContSmooth
                newpath.AppendBezier(p3, p4, p, cont)
                newpath.select_segment(-1)
        else:
            newpath.AppendSegment(type, p12, p, cont)
            newpath.select_segment(-1, path.SegmentSelected(i))
    if path.closed:
        newpath.ClosePath()
        newpath.SetContinuity(-1, path.Continuity(-1))
    return newpath

def copy_selection(path, newpath):
    for i in range(path.len):
        newpath.select_segment(i, path.SegmentSelected(i))

def segments_to_lines(path):
    newpath = CreatePath()
    newpath.AppendLine(path.Node(0))
    for i in range(1, path.len):
        if path.SegmentSelected(i) and path.SegmentSelected(i - 1):
            if path.SegmentType(i) == Bezier:
                cont = ContAngle
                newpath.SetContinuity(-1, ContAngle)
            else:
                cont = path.Continuity(i)
            newpath.AppendLine(path.Node(i), cont)
        else:
            apply(newpath.AppendSegment, path.Segment(i))
    if path.closed:
        cont = newpath.Continuity(-1)
        newpath.ClosePath()
        newpath.SetContinuity(-1, cont)
    copy_selection(path, newpath)
    return newpath

def segments_to_beziers(path):
    f13 = 1.0 / 3.0;	f23 = 2.0 / 3.0
    newpath = CreatePath()
    newpath.AppendLine(path.Node(0))
    for i in range(1, path.len):
        type, p12, p, cont = path.Segment(i)
        if path.SegmentSelected(i) and path.SegmentSelected(i - 1):
            cont = path.Continuity(i)
            if type == Line:
                node1 = path.Node(i - 1); node2 = path.Node(i)
                p1 = f23 * node1 + f13 * node2
                p2 = f13 * node1 + f23 * node2
                cont = ContAngle
            else:
                p1, p2 = p12
            newpath.AppendBezier(p1, p2, p, cont)
        else:
            newpath.AppendSegment(type, p12, p, cont)
    if path.closed:
        cont = newpath.Continuity(-1)
        newpath.ClosePath()
        newpath.SetContinuity(-1, cont)
    copy_selection(path, newpath)
    return newpath

def set_continuity(path, cont):
    f13 = 1.0 / 3.0;	f23 = 2.0 / 3.0
    newpath = path.Duplicate()
    for i in range(1, path.len):
        if path.SegmentSelected(i):
            newpath.SetContinuity(i, cont)
            if cont == ContAngle:
                continue
            if newpath.SegmentType(i) != Bezier:
                continue
            if i == path.len - 1:
                if newpath.closed:
                    other = 1
                else:
                    continue
            else:
                other = i + 1
            if newpath.SegmentType(other) != Bezier:
                continue
            type, (p1, p2), node, oldcont = newpath.Segment(i)
            type, (p3, p4), other_node, other_cont = newpath.Segment(other)

            d = p3 - p2
            if cont == ContSymmetrical:
                d = 0.5 * d
            p2 = adjust_control_point(p2, node, node + d, cont)
            p3 = adjust_control_point(p3, node, node - d, cont)
            newpath.SetBezier(i, p1, p2, node, cont)
            newpath.SetBezier(other, p3, p4, other_node, other_cont)
    return newpath


def copy_path(dest, src, start = 0, end = -1, copy_selection = 1):
    if start < 0:
        start = src.len + start
    if end < 0:
        end = src.len + end
    for i in range(start, end + 1):
        type, control, node, cont = src.Segment(i)
        dest.AppendSegment(type, control, node, cont)
        if copy_selection:
            dest.select_segment(-1, src.SegmentSelected(i))


def insert_node_at(path, at):
    index = int(at)
    t = at - index
    newpath = CreatePath()
    copy_path(newpath, path, 0, index)
    type, control, node, cont = path.Segment(index + 1)
    if type == Line:
        newpath.AppendLine((1 - t) * path.Node(index) + t * node)
        newpath.select_segment(-1)
        newpath.AppendLine(node)
    else:
        if newpath.Continuity(-1) == ContSymmetrical:
            newpath.SetContinuity(-1, ContSmooth)
        p1, p2 = control
        p1, p2, q, p3, p4 = subdivide(newpath.Node(-1), p1, p2, node, t)
        newpath.AppendBezier(p1, p2, q, ContSmooth)
        newpath.select_segment(-1)
        if cont == ContSymmetrical:
            cont = ContSmooth
        newpath.AppendBezier(p3, p4, node, cont)
    copy_path(newpath, path, index + 2)
    if path.closed:
        newpath.ClosePath()
        newpath.SetContinuity(-1, path.Continuity(-1))
    return newpath

def split_path_at(path, at):
    index = int(at)
    t = at - index
    if path.closed:
        path1 = path2 = CreatePath()
        result = [path1]
    else:
        path1 = CreatePath()
        path2 = CreatePath()
        result = [path1, path2]
        copy_path(path1, path, 0, 0, copy_selection = 0)

    type, control, node, cont = path.Segment(index + 1)
    if type == Line:
        q = (1 - t) * path.Node(index) + t * node
        path2.AppendLine(q)
        path2.AppendLine(node)
        path2.select_segment(0)
        function = path1.AppendLine
        args = (q,)
    else:
        p1, p2 = control
        p1, p2, q, p3, p4 = subdivide(path.Node(index), p1, p2, node, t)
        path2.AppendLine(q)
        path2.AppendBezier(p3, p4, node, cont)
        path2.select_segment(0)
        function = path1.AppendBezier
        args = (p1, p2, q, ContSymmetrical)
    copy_path(path2, path, index + 2, copy_selection = 0)
    copy_path(path1, path, 1, index, copy_selection = 0)
    apply(function, args)
    return result

def segment_to_line(path, at):
    index = int(at)
    if path.SegmentType(index + 1) == Bezier:
        newpath = CreatePath()
        copy_path(newpath, path, 0, index)
        newpath.SetContinuity(-1, ContAngle)
        newpath.AppendLine(path.Node(index + 1), ContAngle)
        copy_path(newpath, path, index + 2)
        if path.closed:
            cont = newpath.Continuity(-1)
            newpath.ClosePath()
            newpath.SetContinuity(-1, cont)
    else:
        newpath = path
    return newpath

def segment_to_curve(path, at):
    index = int(at)
    if path.SegmentType(index + 1) == Line:
        newpath = CreatePath()
        copy_path(newpath, path, 0, index)
        f13 = 1.0 / 3.0;
        f23 = 2.0 / 3.0
        node1 = path.Node(index);
        node2 = path.Node(index + 1)
        p1 = f23 * node1 + f13 * node2
        p2 = f13 * node1 + f23 * node2
        newpath.AppendBezier(p1, p2, node2, path.Continuity(index + 1))
        copy_path(newpath, path, index + 2)
        if path.closed:
            cont = newpath.Continuity(-1)
            newpath.ClosePath()
            newpath.SetContinuity(-1, cont)
    else:
        newpath = path
    return newpath



def CombineBeziers(beziers):
    combined = beziers[0].Duplicate()
    paths = combined.paths
    for bezier in beziers[1:]:
        paths = paths + bezier.paths
    combined.paths = paths
    return combined


